package token

import (
	"fmt"
	"net/http"
	"net/url"
	"sync/atomic"
	"time"

	"starsine.cn/appTpl/cmn"
	"starsine.cn/appTpl/cmn/config/aconf"
	. "starsine.cn/appTpl/cmn/defs"
	"starsine.cn/appTpl/cmn/logs"
	"starsine.cn/appTpl/cmn/timermgr"
	"starsine.cn/appTpl/cmn/utils"
	. "starsine.cn/appTpl/svccenter/registry/restful/openapi"
)

type STATE int

func (t STATE) String() string {
	switch t {
	case PERIOD_GETTOKEN:
		return "period-get-token"
	case TIMEOUT_GETTOKEN:
		return "timeout-get-token"
	}
	return "unknown-state"
}

const (
	PERIOD_GETTOKEN STATE = iota
	TIMEOUT_GETTOKEN
)

type Token struct {
	url       url.URL
	token     string
	expire    int64
	sn        atomic.Int32
	svcID     string
	userSpace string
	pswd      string

	smSwitch   chan STATE
	state      STATE
	periodTID  int64
	timeoutTID int64

	httpc   *http.Client
	tm      *timermgr.TimerMgr
	appCtxt *AppContext
}

func CreateToken(appCtxt *AppContext) *Token {
	p := new(Token)
	p.token = ""
	p.sn.Store(0)
	p.expire = time.Now().UnixNano()

	p.smSwitch = make(chan STATE)
	p.state = PERIOD_GETTOKEN
	p.periodTID = -1
	p.timeoutTID = -1

	p.httpc = &http.Client{}
	p.tm = cmn.GetTm()
	p.appCtxt = appCtxt

	p.svcID = aconf.GetString("NodeName", "default", aconf.WithENV("node_name"))
	p.userSpace = aconf.GetString("User", "default", aconf.WithENV("svc_user"))
	p.pswd = aconf.GetString("Pswd", "default", aconf.WithENV("svc_pswd"))

	addr := aconf.GetString("SvcCenter.Addr", "0.0.0.0:20000", aconf.WithENV("sc_addr"))
	path := "/v1/registry/get-token"
	p.url = url.URL{Scheme: "http", Host: addr, Path: path}

	return p
}

func (p *Token) Start() {
	go p.sm()
	p.smSwitch <- PERIOD_GETTOKEN

	cmn.GetEvntEmiter().On("bad-token", func(params ...interface{}) {
		p.smSwitch <- PERIOD_GETTOKEN
	})
}

func (p *Token) valid() bool {
	return p.expire > time.Now().UnixNano()
}

func (p *Token) sm() {
	logs.Infof("restful.Token sm routine start")
	p.appCtxt.Wg.Add(1)
	defer func() {
		logs.Warnf("restful.Token sm routine exit")
		p.appCtxt.Wg.Done()
	}()

	for {
		select {
		case s := <-p.smSwitch:
			logs.Debugf("switch to %s", s)
			switch s {
			case PERIOD_GETTOKEN:
				p.getTokenPeriod()
			case TIMEOUT_GETTOKEN:
				p.getTokenTimeout()
			}

		case <-p.appCtxt.Ctx.Done():
			logs.Warnf("restful.Token sm routine ctxt done to quit")
			return
		}
	}
}

func (p *Token) getTokenPeriod() {
	if p.periodTID != -1 {
		p.tm.CancelTimer(p.periodTID)
	}
	p.periodTID = p.tm.AddPeriodTimer(
		time.Duration(1),
		timermgr.NewOnTimeOut(p.getTokenPeriodCB,
			nil,
		))
}

func (p *Token) getTokenPeriodCB(params interface{}) {
	err := p.getToken()
	if err != nil {
		logs.Debugf("getTokenPeriodCB getToken fail, err: %s", err)
	} else {
		p.tm.CancelTimer(p.periodTID)
		p.periodTID = -1
		p.smSwitch <- TIMEOUT_GETTOKEN
	}
}

func (p *Token) getTokenTimeout() {
	if p.timeoutTID != -1 {
		p.tm.CancelTimer(p.timeoutTID)
	}
	p.timeoutTID = p.tm.AddAfterTimer(
		time.Duration(p.expire-time.Now().UnixNano())/time.Second,
		timermgr.NewOnTimeOut(p.getTokenTimeoutCB,
			nil,
		))
}

func (p *Token) getTokenTimeoutCB(params interface{}) {
	p.timeoutTID = -1
	err := p.getToken()
	if err != nil {
		logs.Debugf("getTokenTimeoutCB getToken fail, err: %s", err)
		p.smSwitch <- PERIOD_GETTOKEN
	} else {
		p.smSwitch <- TIMEOUT_GETTOKEN
	}
}

func (p *Token) getToken() error {
	req := TokenGetRequest{
		MsgHead: RequestHead{
			InstanceID:  "svc-instance-id",
			RequestType: "get-token",
			SerialNum:   p.sn.Load(),
		},
		Data: SvcAuthoInfo{
			UserSpace: p.userSpace,
			SvcID:     p.svcID,
			Key:       p.pswd,
		},
	}
	resp := TokenGetResponse{}

	err, _ := utils.JsonHttpPostWithHeader(p.httpc, p.url.String(),
		map[string]string{
			"Content-Type": "application/json;charset=utf-8",
			"X-Auth-Token": "get-token",
		}, req, &resp)
	if err != nil {
		logs.Errorf("JsonHttpPostWithHeader(%s), err: %s", p.url.String(), err)
		return err
	}

	if resp.MsgHead.Status == "ok" {
		p.token = resp.Data.Token
		p.expire = resp.Data.Expire
		logs.Infof("token get: %s, expire: %s", p.token, time.UnixMicro(p.expire/int64(time.Microsecond)).String())
		err = nil

	} else {
		logs.Warnf("token get fail: %v", resp)
		err = fmt.Errorf("token get fail, err: %s", resp.MsgHead.Message)
	}

	return err
}
